import { GeneratorFunction } from "./genfunction.js";
import { compileScript, getScriptEvent, Scope } from "./script-compiler.js";
import { Costume } from "./costume.js";
import { List } from "./list.js";

function _scriptRandom(min, max) {
    return Math.floor(Math.random() * (max - min)) + min;
}

function hyperDyadic(f, ...args) { // Dyadic is not a real word i swear
    var res = args[0];
    var isArr = res.isSnapList;
    var arr0, arr1;

    for (let i = 1; i < args.length; i++) {
        let arg = args[i];

        if (isArr) {
            if (arg.isSnapList) {
                // target array is asrray with least items
                arg = arg.asArray();

                if (arg.length > res.length) {
                    arr0 = res;
                    arr1 = arg;
                } else {
                    arr0 = arg;
                    arr1 = res;
                }

                res = arr0._map((v, i) => f(v, arr1._get(i)))
            } else {
                res = res._map(v => f(v, arg));
            }
        } else {
            if (arg.isSnapList) {
                isArr = true;
                res = arg.asArray()._map(v => f(res, v));
            } else {
                res = f(res, arg);
            }
        }
    }

    return res;
}

function _scriptSum(...args) {
    return hyperDyadic((a, b) => a + b, ...args);
}

function _scriptProduct(...args) {
    return hyperDyadic((a, b) => a * b, ...args);
}

function _scriptSubtract(a, b) {
    return hyperDyadic((a, b) => a - b, a, b);
}

function _scriptDivide(a, b) {
    return hyperDyadic((a, b) => a / b, a, b);
}

const SCRIPT_OP_MAPPINGS = {
    abs: Math.abs,
    neg: v => -v,
    sign: Math.sign,
    ceiling: Math.ceil,
    floor: Math.floor,
    sqrt: Math.sqrt,
    sin: v => Math.sin(+v / 180 * Math.PI),
    cos: v => Math.cos(+v / 180 * Math.PI),
    tan: v => Math.tan(+v / 180 * Math.PI),
    asin: v => Math.asin(v) / Math.PI * 180,
    acos: v => Math.acos(v) / Math.PI * 180,
    atan: v => Math.atan(v) / Math.PI * 180,
    ln: Math.log,
    log: Math.log10,
    lg: Math.log2,
    ["e^"]: Math.exp,
    ["10^"]: v => 10^(+v),
    ["2^"]: v => 2^(+v),
    id: (item) => {
        if (item instanceof List) {
            return [...item];
        }
    
        return item;
    },
}

class SnapObject {
    constructor(parent) {
        this.name = "Object";
        this.varScope = new Scope(parent.varScope);

        this.parent = parent;
        this.costumes = [];
        this.sounds = [];
        this.functions = new Map();
        this.listeners = {};

        this.curCostume = null;
        this.shown = true;
        this.effects = {
            color: 0,
            saturation: 0,
            brightness: 0,
            fisheye: 0,
            whirl: 0,
            pixelate: 0,
            mosaic: 0,
            ghost: 0,
            negative: 0
        };

        var self = this;

        this.scriptEnv = {
            ops: SCRIPT_OP_MAPPINGS,
            List: List,

            *wait(s) {
                if (s === undefined) {
                    s = 0;
                } else {
                    s = +s;
                    if (Number.isNaN(s))
                        throw new TypeError("Not a number");
                }

                if (self.getProject().warped) return;

                yield ["yield", 1000*s];
            },

            *warp(f) {
                self.getProject().pushWarp();
                yield* f();
                self.getProject().popWarp();
            },

            sum: _scriptSum,
            product: _scriptProduct,
            subtract: _scriptSubtract,
            divide: _scriptDivide,
            random:  _scriptRandom,

            lastAnswer() {
                return self.getProject().lastAnswer;
            },

            *callFunc(opcode, ...args) {
                var f = self.getFunction(opcode).func;

                return yield* f(...args);
            },

            *callFuncYield(opcode, ...args) {
                var f = self.getFunction(opcode).func;

                if (!self.getProject().warped) {
                    yield ["yield", 0];
                }

                return yield* f(...args);
            },

            mouseX() {
                return self.getProject().mouseX;
            },

            mouseY() {
                return self.getProject().mouseY;
            },

            mouseDown() {
                return self.getProject().mouseDown;
            },

            keyPressed(key) {
                return self.getProject().isKeyDown(key);
            },

            resetTimer() {
                self.getProject().timer = 0;
            },

            timer() {
                return self.getProject().timer / 1000;
            },

            setCostume(name) {
                console.log(self.costumes);
                if (name === "Turtle") {
                    self.curCostume = null;
                } else {
                    self.curCostume = self.getCostume(name);
                }
            },

            nextCostume() {
                var idx = self.getCostumeIndex();
                if (idx >= 0) {
                    self.curCostume = self.costumes[(idx + 1) % self.costumes.length];
                }
            },

            show() {
                self.shown = true;
            },

            hide() {
                self.shown = false;
            },

            changeEffect(effect, val) {
                self.effects[effect] += val / 100;
            },

            setEffect(effect, val) {
                self.effects[effect] = val / 100;
            },

            getEffect(effect) {
                return self.effects[effect] * 100;
            },

            clearEffects() {
                self.effects.color = 0;
                self.effects.saturation = 0;
                self.effects.brightness = 0;
                self.effects.ghost = 0;
                self.effects.fisheye = 0;
                self.effects.whirl = 0;
                self.effects.pixelate = 0;
                self.effects.mosaic = 0;
                self.effects.negative = 0;
            },
        }
    }

    async loadXml(xml, shared) {
        shared.refs.set(xml.attributes.id.nodeValue, this);

        this.name = xml.attributes.name.nodeValue;

        for (let child of xml.children) {
            switch (child.nodeName) {
                case "variables": {
                    // TODO
                    break;
                }

                case "sounds": {
                    // TODO
                    break;
                }

                case "costumes": {
                    let listNode = child.firstElementChild;
                    
                    for (let itemNode of listNode.children) {
                        let costumeNode = itemNode.firstElementChild;
                        let costume = new Costume(costumeNode.attributes.name.nodeValue);
                        await costume.load(costumeNode.attributes.image.nodeValue);
                        this.costumes.push(costume);
                    }
                    break;
                }

                case "scripts": {
                    for (let scriptNode of child.children) {
                        // TODO
                        let evData = getScriptEvent(scriptNode);
                        if (evData) {
                            console.group("script compilation");

                            try {
                                let scope = new Scope(this.varScope);

                                // add event parameters into scope
                                let safeParamNames = [];

                                for (let paramName of evData.params) {
                                    safeParamNames.push(scope.addVariable(paramName));
                                }

                                let compiled = compileScript(scriptNode, scope, this.scene.functions);
                                console.log(compiled);

                                let func = new GeneratorFunction("env", ...safeParamNames, compiled);

                                /*this.onEvent(evData.name, (val) => {
                                    if (val === evData.value) {
                                        console.log("Run", func);
                                        this.getProject().runTask(func(this.scriptEnv));
                                    }
                                });*/
                                console.log(evData);
                                this.bindEvent(evData.name, evData.value, func, this.scriptEnv);
                            } catch(err) {
                                console.error(err);
                            }

                            console.groupEnd("script compilation");
                        }
                    }

                    break;
                }
            }
        }

        // set current costume
        var costumeNum = +xml.attributes.costume.nodeValue;
        this.curCostume = costumeNum > 0 ? this.costumes[costumeNum-1] : null;
    }

    getProject() {
        throw new Error("getProject() not implemented");
    }

    getFunction(fname) {
        var res = this.functions.get(fname);
        if (res) return res.func;

        return this.parent.getFunction(fname);
    }

    getCostumeIndex() {
        if (!this.curCostume) return -1;
        return this.costumes.indexOf(this.curCostume);
    }

    getCostume(name) {
        return this.costumes.find(v => v.name == name);
    }

    /*
    onEvent(name, callback) {
        var arr = this.listeners[name];

        if (!arr) {
            arr = [];
            this.listeners[name] = arr;
        }

        arr.push(callback);
    }
    */

    applyEffects(ctx) {
        ctx.globalAlpha = 1 - this.effects.ghost;
    }

    bindEvent(name, value, func, ...args) {
        var arr = this.listeners[name];

        if (!arr) {
            arr = [];
            this.listeners[name] = arr;
        }

        arr.push({val: value, func: func, args: [...args]});
    }

    fireEvent(name, value, ...args) {
        var callbacks = this.listeners[name];

        this.scriptEnv.globals = this.scene.variables;
        
        if (callbacks) {
            for (let callback of callbacks) {
                if (value === callback.val) {
                    console.log(...callback.args, ...args);
                    this.getProject().runTask(callback.func(...callback.args, ...args));
                }
            }
        }
    }

    *fireEventSync(name, value, ...args) {
        var callbacks = this.listeners[name];
        
        if (callbacks) {
            for (let callback of callbacks) {
                if (value === callback.val) {
                    yield* callback.func(...callback.args);
                }
            }
        }
    }
}

export { SnapObject }